Repository navigation
Kernel regularization: opt-in Cholesky-solve evidence term + relative jitter - #437
Conversation
The kernel regularization schemes build `H = coefficient * C^-1` as an explicit dense inverse. On clustered mesh vertices (the kNN families' traced vertices, cond(C) ~ 1e9) that inverse carries round-off amplified by cond(C), which then enters the Bayesian evidence through the regularization term `s^T H s`. For these schemes the term is `coefficient * s^T C^-1 s`, obtainable from a single Cholesky *solve* against `s` rather than by forming `C^-1` and contracting it — one triangular solve instead of N, and accurate to machine precision. Measured on a clustered fixture at cond(C) = 3.2e9: relative error 5.99e-08 (explicit) vs 2.93e-16 (implicit). This is the `s^T H s` counterpart of the log-det shortcut shipped in #391, and follows its shape exactly: a `regularization_term_from` hook on `AbstractRegularization` returning `None` by default, overridden by the four kernel schemes, consulted by `AbstractInversion.regularization_term` behind a new opt-in `Settings.regularization_term_method` ("matmul" default, "cho_solve" opt-in). Default evidence values are unchanged, preserving comparability with archived results. The setting is deliberately separate from `log_det_method` so the two evidence terms can be moved onto their exact factorizations independently, which is what makes an evidence shift attributable to one term rather than both. Scope note: this does not remove the explicit inverse from the inversion as a whole. `curvature_reg_matrix` is a dense `F + H` feeding the dense solve for the reconstruction, so `H` is still formed there; only the evidence terms avoid it. Notes on the per-scheme overrides: - `MaternAdaptKernel` needs its own override rather than inheriting `MaternKernel`'s: it passes `coefficient=0.0` to the parent (its adaptive weights live inside the covariance), so the inherited form would zero the term. Pinned by a regression test. - `GaussianKernel`'s shortcut is the term of the analytic `coefficient * C^-1`, excluding the symmetrisation and trace-scaled jitter its formed matrix carries — consistent with how its log-det shortcut already behaves, since both exist only to guard a factorization this path avoids. - Schemes with no factorization (`Constant`, `Adapt`, the split families) return `None`, and one `None` makes the whole computation fall back to the formed matrix, so mixed inversions stay correct. Tests: 9 new cases in test_kernel_regularization_term.py covering shortcut == formed matrix for all four schemes, the gate in both directions, the fallback, the MaternAdapt zero-coefficient trap, and the ill-conditioning gain graded against an exactly-known reference (`s = C v` makes `C^-1 s = v`). Full inversion suite green: 244 passed, 9 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KazMzMZYPLfaZoYQ79YQ8Q
Leg 2b. The kernel covariance jitter is a fixed absolute `1e-8 * I`. That is
only meaningful when the covariance diagonal is itself ~1, which holds for the
unweighted kernels (K(0) == 1) but NOT for `MaternAdaptKernel`, whose
`C_ii = w_i^2` spans the adaptive-weight dynamic range.
Measured on a 40-pixel adaptive fixture, faintest pixel's relative distortion
from the jitter:
inner/outer C_ii (faintest) distortion
1.0 / 1.0 1.0e+00 1.0e-08
0.5 / 4.0 3.9e-03 2.6e-06
1.0 / 20.0 6.3e-06 1.6e-03
0.1 / 100.0 1.0e-08 1.0e+00 <- 100% of that pixel's variance
`inner_coefficient` and `outer_coefficient` are free model parameters, so a
sampler can walk into the bottom row mid-fit and silently destroy the kernel
structure of the faintest pixels.
Fix: `jitter_relative=True` applies `jitter * diag(diag(C))`, i.e.
`C_ii *= (1 + jitter)`. Writing `C = D^(1/2) R D^(1/2)` for the correlation
matrix R, this is exactly `D^(1/2) (R + jitter I) D^(1/2)` — the jitter lands on
the correlation matrix, so every pixel gets the same relative protection
whatever its scale.
Two properties verified rather than assumed:
- It does not weaken the Cholesky protection. On clustered (traced) vertices at
nu=2.5 the conditioning is unchanged (3.16e9 both conventions). An earlier
candidate rule (jitter = N * eps * max(diag), "as small as possible") was
rejected for exactly this: it let cond(C) reach 3.2e15, at the edge of
float64, reintroducing the noise leg 2 exists to remove.
- On the three unweighted schemes the two conventions agree to ~1e-8, since
their diagonal is ~1 by construction.
Default is `False` everywhere, byte-identical to previous behaviour, so
archived evidence values are unchanged. Threaded through all three covariance
call sites per scheme (matrix, log-det shortcut, term shortcut), not just the
constructor — pinned by a test.
Tests: 7 new cases in test_kernel_jitter_relative.py. Full suite 917 passed,
58 skipped (3 pre-existing pynufft failures unrelated, missing optional dep).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KazMzMZYPLfaZoYQ79YQ8Q
Closes the JAX leg of this work's gate at library level. The workspace script `autolens_workspace_test/scripts/imaging/jax_grad/regularization.py` certifies the same surface end-to-end, but it lives in another repo and needs the whole autolens stack; these cover the code this branch actually adds. Measured: FD certification d(s^T C^-1 s)/d(scale) rel diff 4.6e-09 eager vs jit 3.6e-15 implicit vs explicit inverse (JAX) 1.1e-13 numpy vs jax parity 8.4e-12 Covers both jitter conventions under `jit` (the `jitter_relative` branch must resolve at trace time, not on a tracer), gradients through the scheme-level `regularization_term_from` hook, and FD certification with relative jitter on. Matern is deliberately not covered: its JAX path needs the modified Bessel from `tfp-nightly`, an optional-of-an-optional. Gaussian and Exponential exercise the same shared `apply_jitter` / `quadratic_form_via_cholesky` code without it. Skipped via importorskip when JAX is absent. Full suite with JAX installed: 936 passed, 52 skipped (3 pre-existing pynufft failures unrelated, missing optional dep). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KazMzMZYPLfaZoYQ79YQ8Q
|
CI is red on Both jobs fail on a single test: The same test fails identically on
Same test, same assertion, +30 passing tests from this branch and no new failures. Why it is not this branch. The assertion is the test's own sanity check at It requires
Not fixing it here. Deciding what that sanity assertion should be (drop it, seed-tune it, or make the guard deterministic) is a judgement about a different subsystem and belongs in its own change rather than being folded into this one. Happy to file it separately. Generated by Claude Code |
Closes leg 2 of PyAutoMind
draft/feature/autoarray/regularization_jax_gradient_gaps.md.Both changes are opt-in and default-off, so default evidence values are unchanged and archived results stay comparable — the constraint the #391 adversarial probe verdict imposed.
Leg 2a —
s^T H svia one Cholesky solveThe kernel schemes define
H = coefficient * C^-1and were forming that inverse explicitly. Forming an inverse is slower and less accurate than solving: the error is amplified bycond(C), which reaches ~1e9 on the clustered traced vertices of the kNN mesh families, putting a noise floor on the likelihood the sampler sees.regularization_termnow computescoefficient * s^T C^-1 sfrom a single Cholesky solve againsts. Shape mirrors the log-det shortcut shipped in #391 exactly: aregularization_term_fromhook onAbstractRegularizationreturningNoneby default, overridden by the four kernel schemes, consulted behind a newSettings.regularization_term_method("matmul"default,"cho_solve"opt-in).Measured on a clustered fixture at
cond(C) = 3.2e9, graded against an exactly-known reference (choosings = CvmakesC^-1 s = v, so the true form iss^T vand needs no inverse anywhere):Why a separate setting from
log_det_methodDeliberate. The two evidence terms can be moved onto their exact factorizations independently, which is what makes an evidence shift attributable to one term rather than both. Gating
s^T H son a setting named for log-determinants would also have been a lie in the name.Scope limit worth stating
This does not remove the explicit inverse from the inversion as a whole.
curvature_reg_matrixis a denseF + Hfeeding the dense solve for the reconstruction, soHis still formed there. Only the evidence terms avoid it. Doing better needs an iterative-solver design — a different piece of work.Leg 2b — relative jitter
The covariance jitter was a fixed absolute
1e-8 * I, which only makes sense whendiag(C) ~ 1. Measured: true for the three unweighted kernels (K(0) == 1), false forMaternAdaptKernel, whoseC_ii = w_i^2spans the adaptive-weight dynamic range.Distortion of the faintest pixel, 40-pixel fixture:
C_iiinner_coefficient/outer_coefficientare free model parameters, so a sampler can reach that bottom row mid-fit — silently, with no exception and no NaN, just wrong smoothing.jitter_relative=TrueappliesC_ii *= (1 + jitter). SinceC = D^(1/2) R D^(1/2), that is exactlyD^(1/2) (R + jitter I) D^(1/2)— the jitter lands on the correlation matrix, so every pixel gets the same relative protection whatever its scale.A rejected alternative, recorded so it isn't retried:
jitter = N * eps * max(diag)("as small as possible above the round-off floor") fixes the distortion but letscond(C)reach 3.2e15, at the edge of float64 — reintroducing exactly the noise this branch exists to remove. The correlation-relative rule leaves conditioning unchanged (3.16e9 both conventions, measured).Two traps found while implementing, both pinned by tests
MaternAdaptKernelpassescoefficient=0.0toMaternKernel.__init__(its weights live insideC_w). Inheriting the parent's term would have silently zeroed it — wrong but finite, the worst failure mode. It needs its own override.GaussianKernel's formed matrix carries a symmetrisation plus trace-scaled jitter that the analytic shortcut excludes — consistent with how its log-det shortcut already behaves, since both exist only to guard a factorization these paths avoid.Schemes with no factorization (
Constant,Adapt, the split families) returnNone, and oneNonefalls the whole computation back to the formed matrix, so mixed inversions stay correct.Testing
29 new tests across three files:
test_kernel_regularization_term.py(9) — shortcut vs formed matrix for all four schemes, the gate in both directions, the fallback, theMaternAdaptzero-coefficient trap, the ill-conditioning gain.test_kernel_jitter_relative.py(7) — default byte-identical to previous behaviour, relative jitter as a pure diagonal rescaling, the adaptive-weight bug, conditioning preserved on clustered vertices, flag threaded to all three covariance call sites per scheme.test_kernel_jax_gradients.py(13) — the JAX leg of the gate.JAX certification:
d(s^T C^-1 s)/d(scale)Full suite: 936 passed, 52 skipped. Three
test_transformer.pyfailures are pre-existing onmainand unrelated (missing optionalpynufft).Matérn's JAX path is not covered by the new JAX tests — it needs the modified Bessel from
tfp-nightly, an optional-of-an-optional. Gaussian and Exponential exercise the same sharedapply_jitter/quadratic_form_via_choleskycode without it. The workspace-levelautolens_workspace_test/scripts/imaging/jax_grad/regularization.pystill covers the Matérn end-to-end and is unaffected by this branch's defaults.Generated by Claude Code